package dirwatch

import (
	"context"
	"github.com/pkg/errors"
	"log"
	"os"
	"path/filepath"
	"strings"
	"sync"

	"vov.cx/kagura/utils/fileutils"

	"github.com/fsnotify/fsnotify"
)

type Watcher struct {
	dir       string
	fsw       *fsnotify.Watcher
	ignores   *fileutils.PatternMatcher
	onChanged func(ev fsnotify.Event)
	done      chan struct{}
	isClosed  bool
	mu        sync.Mutex
}

func New(dir string) (*Watcher, error) {
	fsw, err := fsnotify.NewWatcher()
	if err != nil {
		return nil, errors.WithStack(err)
	}
	w := &Watcher{
		dir:  dir,
		fsw:  fsw,
		done: make(chan struct{}),
	}

	if err := w.loadIgnore(); err != nil {
		log.Println(err) //ignore the error
	}

	if err := w.watchDIR(); err != nil {
		_ = w.Close()
		return nil, err
	}

	return w, nil
}

func (w *Watcher) loadIgnore() error {
	patterns, err := fileutils.ReadFileLines(filepath.Join(w.dir, "build.ignore"))
	if err != nil {
		return errors.WithStack(err)
	}
	ignore, err := fileutils.NewPatternMatcher(patterns)
	if err != nil {
		return errors.WithStack(err)
	}
	w.ignores = ignore
	return nil
}

// addDIR 监控目录
func (w *Watcher) watchDIR() error {
	err := filepath.Walk(w.dir, func(path string, info os.FileInfo, err error) error {
		if info.IsDir() && !w.isIgnored(path) {
			if path, err = filepath.Abs(path); err != nil {
				return errors.WithStack(err)
			}
			if err = w.fsw.Add(path); err != nil {
				return errors.WithStack(err)
			}
		}
		return nil
	})
	return errors.WithStack(err)
}

func (w *Watcher) Watch(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return w.Close()
		case <-w.done:
			break
		case ev := <-w.fsw.Events:
			if ev.Name == "" && ev.Op == 0 {
				break
			}
			w.onEvent(ev)
		case err := <-w.fsw.Errors:
			return errors.WithStack(err)
		}
	}
}

func (w *Watcher) SetOnChanged(handler func(ev fsnotify.Event)) {
	w.onChanged = handler
}

func (w *Watcher) Close() error {
	w.mu.Lock()
	defer w.mu.Unlock()
	if w.isClosed {
		return nil
	}
	w.isClosed = true
	close(w.done)
	return errors.WithStack(w.fsw.Close())
}

func (w *Watcher) onEvent(ev fsnotify.Event) {
	if ev.Name == "build.ignore" {
		if isRemoved(ev) {
			w.ignores = nil
		}
		if isModified(ev) {
			if err := w.loadIgnore(); err != nil {
				log.Println(err)
			}
		}
		return
	}

	if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
		return
	}

	if w.isIgnored(ev.Name) {
		return
	}

	switch {
	case ev.Op&fsnotify.Rename == fsnotify.Rename:
		_ = w.fsw.Remove(ev.Name)
	case ev.Op&fsnotify.Create == fsnotify.Create:
		if file, err := os.Stat(ev.Name); err == nil && file.IsDir() {
			_ = w.fsw.Add(ev.Name)
		}
	case ev.Op&fsnotify.Remove == fsnotify.Remove:
		if fi, err := os.Stat(ev.Name); err == nil && fi.IsDir() {
			_ = w.fsw.Remove(ev.Name)
		}
	}

	go func(ev fsnotify.Event) {
		if w.onChanged != nil {
			w.onChanged(ev)
		}
	}(ev)
}

func (w *Watcher) onError(err error) {
	log.Printf("watch error : %+v\n", err)
}

func (w *Watcher) isIgnored(name string) bool {
	if w.ignores != nil {
		if strings.HasPrefix(name, w.dir) {
			if fixed, _ := filepath.Rel(w.dir, name); fixed != "" {
				name = fixed
			}
		}
		ignore, _ := w.ignores.Matches(name)
		return ignore
	}
	return false
}

func isRemoved(ev fsnotify.Event) bool {
	return ev.Op&fsnotify.Rename == fsnotify.Rename ||
		ev.Op&fsnotify.Remove == fsnotify.Remove
}

func isModified(ev fsnotify.Event) bool {
	return ev.Op&fsnotify.Create == fsnotify.Create ||
		ev.Op&fsnotify.Write == fsnotify.Write
}
